/**
 * 
 */
package org.codesketch.birch.common;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;

import org.codesketch.birch.ssh.File;
import org.codesketch.birch.ssh.Response;
import org.codesketch.birch.ssh.Statement;
import org.codesketch.birch.ssh.UploadStatement;

import com.amazonaws.services.ec2.model.InstanceStateName;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

/**
 * @author quirino
 *
 */
public class Instance {

	private com.amazonaws.services.ec2.model.Instance instance;
	private Session session;
	private String username;
	private String password;
	private Boolean usePk;
	
	/**
	 * @param instance
	 * @param session
	 * @param username
	 * @param privateKey
	 * @param usePk
	 * @throws IOException 
	 */
	private Instance(com.amazonaws.services.ec2.model.Instance instance,
			String username, String privateKey) {
		this.instance = instance;
		this.username = username;
		this.password = privateKey;
		this.usePk = true;
	}
	
	/**
	 * @return the instance
	 */
	public com.amazonaws.services.ec2.model.Instance getInstance() {
		return instance;
	}
	/**
	 * Return the value of the name tag.
	 * @return the name tag value as a {@link String}.
	 */
	public String getInstanceKeyName() {
		return instance.getKeyName();
	}
	/**
	 * Return the instance state
	 */
	public InstanceStateName state() {
		return InstanceStateName.fromValue(instance.getState().getName());
	}
	/**
	 * Return the instance identifier.
	 * @return the instance identifier as a {@link String}.
	 */
	public String getInstanceId() {
		return instance.getInstanceId();
	}
	
	public void destroy() {
		if(null != session) {
			session.disconnect();
		}
	}
	
	/**
	 * Executes the given statement on this instance.
	 * @param statement the statement to have to execute.
	 * @return the execution {@link Response}
	 * @throws IOException if an error occours.
	 */
	public Response<String> execute(final Statement statement) {
		Response<String> response;
		if(StringUtils.empty(statement.statement())) {
			response = Response.with(1,"Statement cannot be empty");
		}
		if(connect()) {
			ChannelExec channel = null;
			BufferedReader reader = null;
			try {
				// open channel
				channel = (ChannelExec) session.openChannel("exec");
				channel.setPty(true);
				// build command
				final StringBuilder builder = new StringBuilder();
				if(!StringUtils.empty(statement.directory())) {
					builder.append(statement.directory());
					builder.append("\n");
				}
				builder.append(statement.statement());
				channel.setCommand(builder.toString());
				// get server stream
				channel.setInputStream(null, true);
				reader = new BufferedReader(new InputStreamReader(channel.getInputStream()));
				// connect with timeout 1 minute
				channel.connect(60000);
				
				response = waitFor(channel, reader);
			} catch (Exception e) {
				response = Response.with(1, e.getMessage());
			} finally {
				if(null!=channel) { channel.disconnect(); }
				if(null!=reader) { try { reader.close(); } catch (IOException e) { } }
			}
		} else {
			// TODO can be local check the IP address
			response = Response.with(1, "Session is not open");
		}
		return response;
	}
	/**
	 * Upload the requested file on this instance.
	 * @param statement the {@link UploadStatement} to have to execute
	 * @return a {@link Response} that defines the upload process outcome
	 * @throws IOException if an error occurs.
	 */
	public Response<String> upload(
			final UploadStatement statement) {
		Response<String> response;
		if(connect()) {
			ChannelSftp channel = null;
			try {
				channel = (ChannelSftp) session.openChannel("sftp");
				channel.setPty(true);
				// connect with timeout 1 minute
				channel.connect(60000);
				if(!StringUtils.empty(statement.directory())) {
					channel.cd(statement.directory());
				}
				
				for( final File file : statement.source() ) {
					channel.put(file.getStream(), file.getName());
				}
				
				response = Response.with(0, "File(s) uploaded correctly");
			} catch(Exception e) {
				response = Response.with(1, e.getMessage());
			} finally {
				if(null!=channel) { channel.disconnect(); }
			}
		} else {
			// TODO can be local check the IP address
			response = Response.with(1, "Session is not open");
		}
		return response;
	}
	/**
	 * Create a new {@link Instance}.
	 * @param instance the {@link com.amazonaws.services.ec2.model.Instance}
	 * @return a new {@link Instance}
	 * @throws IOException 
	 */
	public static final Instance with(com.amazonaws.services.ec2.model.Instance instance) {
		return new Instance(instance, "", "");
	}
	/**
	 * Create a new {@link Instance}
	 * @param instance the {@link Instance}
	 * @param username the SSH username
	 * @param password the SSH password or primary key path
	 * @param usePk flag tell if the SSH connection should be established with private key or password
	 * @return a new {@link Instance}
	 * @throws IOException 
	 */
	public static final Instance with(com.amazonaws.services.ec2.model.Instance instance,
			String username, String privateKey) {
		return new Instance(instance, username, privateKey);
	}
	
	// -- Helper
	private Response<String> waitFor(final Channel channel, final BufferedReader reader) {
		String message;
		if(channel.isClosed()) {
			message = "";
		} else {
			try {
				message = read(reader);
			} catch (IOException e1) { message = ""; }
			while(!channel.isClosed()) {
				try {
					Thread.sleep(2);
				} catch (InterruptedException e) {}
			}
		}
		return Response.with(channel.getExitStatus(), message);
	}
	
	private String read(final BufferedReader reader) throws IOException {
		final StringBuilder builder = new StringBuilder();
		if(reader.ready()) {
			while(true) {
				String line = reader.readLine();
				if(null==line) {
					break;
				}
				builder.append(line);
				builder.append("\n");
			}
		}
		return builder.toString();
	}
	/**
	 * Connect to the remote host if and only if provided username and password 
	 * are not empty (null or empty string).
	 */
	private boolean connect() {
		boolean connected = false;
		// Create the SSH client only if username and password are provided
		if(!StringUtils.empty(username, password)) {
			if(null!=session) {
				connected = session.isConnected();
			}
			if(!connected) {
				final JSch jsch = new JSch();
				final Properties config = new Properties();
				config.put("StrictHostKeyChecking", "no");
	//					config.put("compression.s2c", "zlib,none");
	//					config.put("compression.c2s", "zlib,none");
				try {
					// opens a SSH session on port 22 assuming this is the default port
					session = jsch.getSession(username, 
							instance.getPublicDnsName(), 22);
					session.setConfig(config);
					if(usePk) {
						// that case the password represent the path to the private key
						jsch.addIdentity(password);
					} else {
						session.setPassword(password);
					}
					connectAndRetry(session);
					connected = true;
				} catch (JSchException e) { connected = false; }
			}
		} else {
			connected = false;
		}
		return connected;
	}
	
	private void connectAndRetry(final Session session) {
		int retry = 0, maxRetry = 10;
		do {
			try {
				// connect with timeout 5 minutes
				session.connect(300000);
			} catch (JSchException r) {
				if(retry++ > maxRetry) { break; }
				try { Thread.sleep(1000); } catch (InterruptedException e) { break; }
			}
		} while(!session.isConnected());
	}
}
